package org.batfish.specifier.parboiled;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import org.batfish.datamodel.Protocol;
import org.batfish.datamodel.applications.Application;
import org.batfish.datamodel.applications.IcmpTypeCodesApplication;
import org.batfish.datamodel.applications.IcmpTypesApplication;
import org.batfish.datamodel.applications.TcpApplication;
import org.batfish.datamodel.applications.UdpApplication;
import org.batfish.specifier.ApplicationSpecifier;
import org.batfish.specifier.Grammar;
import org.parboiled.errors.InvalidInputError;
import org.parboiled.parserunners.ReportingParseRunner;
import org.parboiled.support.ParsingResult;

/** An {@link ApplicationSpecifier} that resolves based on the AST generated by {@link Parser}. */
@ParametersAreNonnullByDefault
public final class ParboiledAppSpecifier implements ApplicationSpecifier {

  @ParametersAreNonnullByDefault
  private static final class AppAstNodeToApps implements AppAstNodeVisitor<Set<Application>> {

    AppAstNodeToApps() {}

    @Override
    public Set<Application> visitIcmpAllAppAstNode(IcmpAllAppAstNode icmpAllAppAstNode) {
      return ImmutableSet.of(IcmpTypesApplication.ALL);
    }

    @Override
    public Set<Application> visitIcmpTypeAppAstNode(IcmpTypeAppAstNode icmpTypeAppAstNode) {
      return ImmutableSet.of(new IcmpTypesApplication(icmpTypeAppAstNode.getType()));
    }

    @Override
    public Set<Application> visitIcmpTypeCodeAppAstNode(
        IcmpTypeCodeAppAstNode icmpTypeCodeAppAstNode) {
      return ImmutableSet.of(
          new IcmpTypeCodesApplication(
              icmpTypeCodeAppAstNode.getType(), icmpTypeCodeAppAstNode.getCode()));
    }

    @Override
    public Set<Application> visitRegexAppAstNode(RegexAppAstNode regexAppAstNode) {
      return Arrays.stream(Protocol.values())
          .filter(protocol -> regexAppAstNode.getPattern().matcher(protocol.toString()).find())
          .map(Protocol::toApplication)
          .collect(ImmutableSet.toImmutableSet());
    }

    @Override
    public Set<Application> visitNameAppAstNode(NameAppAstNode nameAppAstNode) {
      return ImmutableSet.of(nameAppAstNode.getNamedApplication().getApplication());
    }

    @Override
    public Set<Application> visitTcpAppAstNode(TcpAppAstNode tcpAppAstNode) {
      return ImmutableSet.of(
          tcpAppAstNode.getPorts().isEmpty()
              ? TcpApplication.ALL
              : new TcpApplication(tcpAppAstNode.getPorts()));
    }

    @Override
    public Set<Application> visitUdpAppAstNode(UdpAppAstNode udpAppAstNode) {
      return ImmutableSet.of(
          udpAppAstNode.getPorts().isEmpty()
              ? UdpApplication.ALL
              : new UdpApplication(udpAppAstNode.getPorts()));
    }

    @Override
    public @Nonnull Set<Application> visitUnionAppAstNode(UnionAppAstNode astNode) {
      return Sets.union(astNode.getLeft().accept(this), astNode.getRight().accept(this));
    }
  }

  private final AppAstNode _ast;

  ParboiledAppSpecifier(AppAstNode ast) {
    _ast = ast;
  }

  /**
   * Returns an {@link ApplicationSpecifier} based on {@code input} which is parsed as {@link
   * Grammar#APPLICATION_SPECIFIER}.
   *
   * @throws IllegalArgumentException if the parsing fails or does not produce the expected AST
   */
  public static ParboiledAppSpecifier parse(String input) {
    ParsingResult<AstNode> result =
        new ReportingParseRunner<AstNode>(
                Parser.instance().getInputRule(Grammar.APPLICATION_SPECIFIER))
            .run(input);

    if (!result.parseErrors.isEmpty()) {
      throw new IllegalArgumentException(
          ParserUtils.getErrorString(
              input,
              Grammar.APPLICATION_SPECIFIER,
              (InvalidInputError) result.parseErrors.get(0),
              Parser.ANCHORS));
    }

    AstNode ast = ParserUtils.getAst(result);
    checkArgument(
        ast instanceof AppAstNode, "ParboiledAppSpecifier requires an IP protocol specifier input");

    return new ParboiledAppSpecifier((AppAstNode) ast);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof ParboiledAppSpecifier)) {
      return false;
    }
    return Objects.equals(_ast, ((ParboiledAppSpecifier) o)._ast);
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(_ast);
  }

  @Override
  public Set<Application> resolve() {
    return _ast.accept(new AppAstNodeToApps());
  }
}
